﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using MoneyBook.ComponentModel;
using MoneyBook.Navigation;
using MoneyBook.Reflection;
using MoneyBook.ComponentModel.DataAnnotations;

namespace MoneyBook {

    public abstract class ViewModelBase : INotifyPropertyChanged, INotifyPropertyChanging, INotifyDataErrorInfo,ICustomTypeProvider {

        private static Dictionary<Type,Type> customTypes = new Dictionary<Type,Type>();
        private static volatile object sync = new object();
      
        protected abstract EnhancePropertyAttributeType GetEnhancePtopertyAttributeType();

        public event PropertyChangedEventHandler PropertyChanged;

        public event PropertyChangingEventHandler PropertyChanging;

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        private Dictionary<string, List<ValidationErrorInfo>> errors = new Dictionary<string, List<ValidationErrorInfo>>();

        public void ValidateAllProperties(bool suppressOnErrorChanged = false) {
            PropertyInfo[] properties = this.GetCustomType().GetProperties(BindingFlags.Instance |
                                                                     BindingFlags.GetProperty |
                                                                     BindingFlags.SetProperty |
                                                                     BindingFlags.Public);

            foreach (PropertyInfo property in properties) {
                object value = property.GetGetMethod().Invoke(this, null);
                ValidateProperty(property, value, suppressOnErrorChanged: suppressOnErrorChanged);
            }
        }

        protected void ValidateProperty(string propertyName, object newValue, bool isBindingValidation = true, bool suppressOnErrorChanged = false) {
            PropertyInfo property = this.GetCustomType().GetProperty(propertyName, 
                                                                     BindingFlags.Instance |
                                                                     BindingFlags.GetProperty |
                                                                     BindingFlags.SetProperty |
                                                                     BindingFlags.Public);
            if (property == null) {
                throw new ArgumentException("property is not found.", "propertyName");
            }
            ValidateProperty(property, newValue, isBindingValidation, suppressOnErrorChanged);
        }

        private void ValidateProperty(PropertyInfo prop, object newValue, bool isBindingValidation = true, bool suppressOnErrorChanged = false) {

            object[] attributes = prop.GetCustomAttributes(typeof(ExcludeValidationAttribute), false);
            if (attributes != null && attributes.Length > 0) {
                return;
            }

            string propertyName = prop.Name;

            List<ValidationResult> results = new List<ValidationResult>();
            //Validator.TryValidateProperty(newValue, new ValidationContext(this, null, null) { MemberName = propertyName }, results);
            CustomValidator.TryValidateProperty(newValue, new ValidationContext(this, null, null) { MemberName = propertyName }, results);
            
            bool isChanged = false;
            List<string> changePropertyNames = new List<string>() { propertyName };

            if (results.Count == 0) {
                if (this.errors.ContainsKey(propertyName)) {
                    List<ValidationErrorInfo> infos = this.errors[propertyName];
                    foreach (ValidationErrorInfo info in infos) {
                        foreach (string changePropertyName in RemoveGroupErrors(info)) {
                            if (!changePropertyNames.Contains(changePropertyName)) {
                                changePropertyNames.Add(changePropertyName);
                            }
                        }
                    }
                    this.errors.Remove(propertyName);
                    isChanged = true;
                }
            } else {

                List<ValidationErrorInfo> infos = GetErrorsList(propertyName);

                foreach (ValidationResult result in results) {
                    if (infos.FirstOrDefault(v => v.Message == result.ErrorMessage) == null) {
                        infos.Add(new ValidationErrorInfo(propertyName, result.ErrorMessage, isBindingValidation, null));
                        isChanged = true;
                    }
                }

                foreach (ValidationErrorInfo info in infos.ToArray()) {
                    if (results.FirstOrDefault(result => result.ErrorMessage == info.Message) == null) {
                        changePropertyNames.AddRange(RemoveGroupErrors(info));
                        infos.Remove(info);
                        foreach (string changePropertyName in RemoveGroupErrors(info)) {
                            if (!changePropertyNames.Contains(changePropertyName)) {
                                changePropertyNames.Add(changePropertyName);
                            }
                        }
                        isChanged = true;
                    }
                }
            }

            if (isChanged && suppressOnErrorChanged) {
                this.OnPropertyChanged("HasErrors");
                this.OnPropertyChanged("HasBindingValidationErrors");
            }

            if (isChanged && !suppressOnErrorChanged) {
                foreach (string target in changePropertyNames) {
                    this.OnErrorChanged(target);
                }
            }
        }

        private List<string> RemoveGroupErrors(ValidationErrorInfo info) {
            List<string> result = new List<string>();
            if (info == null || info.GroupProperties == null) {
                return result;
            }
            foreach (string propertyName in info.GroupProperties) {
                if (propertyName == info.PropertyName) {
                    continue;
                }
                if (!this.errors.ContainsKey(propertyName)) {
                    continue;
                }
                List<ValidationErrorInfo> infos = this.errors[propertyName];
                ValidationErrorInfo target = infos.FirstOrDefault(i => i.Message == info.Message && i.GroupProperties != null && i.GroupProperties.Contains(info.PropertyName));
                if (target != null) {
                    infos.Remove(target);
                    if (infos.Count < 1) {
                        this.errors.Remove(propertyName);
                    }
                    if (!result.Contains(propertyName)) {
                        result.Add(propertyName);
                    }
                }
            }
            return result;
        }

        protected void AddError(string message, params string[] propertyNames) {
            if (propertyNames == null || propertyNames.Length == 0) {
                return;
            }
            foreach (string propertyName in propertyNames) {
                List<ValidationErrorInfo> messages = GetErrorsList(propertyName);
                if (messages.FirstOrDefault(m => m.Message == message) == null) {
                    messages.Add(new ValidationErrorInfo(propertyName, message, false, propertyNames));
                    this.OnErrorChanged(propertyName);
                }
            }
        }

        private List<ValidationErrorInfo> GetErrorsList(string propertyName) {
            if (!this.errors.ContainsKey(propertyName)) {
                this.errors.Add(propertyName, new List<ValidationErrorInfo>());
            }
            return this.errors[propertyName];
        }

        protected void OnPropertyChanged(string propertyName) {
            if (this.PropertyChanged != null) {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        protected void OnPropertyChanging(string propertyName) {
            if (this.PropertyChanging != null) {
                this.PropertyChanging(this, new PropertyChangingEventArgs(propertyName));
            }
        }

        protected void OnErrorChanged(string propertyName) {
            this.OnPropertyChanged("HasErrors");
            this.OnPropertyChanged("HasBindingValidationErrors");
            if (this.ErrorsChanged != null) {
                this.ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
            }
        }

        public IEnumerable GetErrors(string propertyName) {
            if (this.errors.ContainsKey(propertyName)) {
                return this.errors[propertyName];
            }
            return null;
        }

        public bool HasError(string propertyName) {
            return this.errors.ContainsKey(propertyName);
        }

        [ExcludeValidation]
        public bool HasErrors {
            get { return this.errors.Count > 0; }
        }

        [ExcludeValidation]
        public bool HasBindingValidationErrors {
            get {
                if (!this.HasErrors) {
                    return false;
                }
                foreach (string key in this.errors.Keys) {
                    ValidationErrorInfo info = this.errors[key].FirstOrDefault(i => i.IsBindingValidationError);
                    if (info != null) {
                        return true;
                    }
                }
                return false;
            }
        }

        private class ValidationErrorInfo {

            public ValidationErrorInfo(string propertyName, string message, bool isBindingValidationError, string[] groupProperties) {
                this.PropertyName = propertyName;
                this.Message = message;
                this.IsBindingValidationError = isBindingValidationError;
                this.GroupProperties = groupProperties;

            }

            public string PropertyName { get; private set; }

            public string Message { get; private set; }

            public bool IsBindingValidationError { get; private set; }

            public string[] GroupProperties { get; private set; }

            public override string ToString() {
                return this.Message;
            }
        }

        public void Navigate(Uri uri) {
            if (NavigationMessageService.Curret != null) {
                NavigationMessageService.Curret.Navigate(uri);
            }
        }

        public Type GetCustomType() {
            Type sourceType = this.GetType();
            if (!customTypes.ContainsKey(sourceType)) {
                lock (sync) {
                    if (!customTypes.ContainsKey(sourceType)) {
                        Type customType = this.GetEnhancePtopertyAttributeType();
                        if (customType == null) { 
                            customType = sourceType;
                        }
                        customTypes.Add(sourceType, customType);
                    }
                }
            }
            return customTypes[this.GetType()];
        }
    }
}

